Skip to content

Add model-aware reasoning and ChatGPT Pro handoff - #214

Open
Nul-led wants to merge 2 commits into
friuns2:mainfrom
Nul-led:agent/model-picker-pro-handoff
Open

Add model-aware reasoning and ChatGPT Pro handoff#214
Nul-led wants to merge 2 commits into
friuns2:mainfrom
Nul-led:agent/model-picker-pro-handoff

Conversation

@Nul-led

@Nul-led Nul-led commented Jul 30, 2026

Copy link
Copy Markdown

Summary

  • make the model and reasoning pickers consume model/list metadata, preserving server order/defaults and exposing Max/Ultra for models such as GPT-5.6 Sol while retaining a conservative fallback for provider-only models
  • add Continue in ChatGPT Pro… to the selected-thread menu; it copies a bounded handoff containing the persisted transcript, active Goal, repository status/commits/diff, changed files, and applicable AGENTS.md instructions, then opens ChatGPT
  • keep repository inspection read-only, explicitly user-triggered, time-bounded, and exclude raw reasoning from the handoff

Validation

  • pnpm exec vitest run src/api/codexGateway.test.ts src/composables/useDesktopState.test.ts src/server/proHandoff.test.ts — 50/50 passed on the clean upstream branch
  • pnpm run build — passed
  • CJS/public entry smoke: node dist-cli/index.js --help — passed
  • Playwright at 1440×1000 verified Max/Ultra visibility, unsupported-effort filtering, the selected-thread action, clipboard copy, popup navigation, and light/dark themes
  • full suite: 149/151 passed; the two remaining assertions are existing Windows-only path-casing and POSIX-mode expectations
  • Docker provider matrix was not run because Docker Desktop's Linux engine was unavailable on this machine

Performance

  • browser runtime profile completed with no warnings and one request each for thread list, skills, rate limits, and provider models (218.7 KB total API traffic)
  • the handoff endpoint runs only after the explicit menu action; a live 220,654-character handoff completed in 1.76 seconds
  • Git commands have 10-second timeouts, output caps, and external diff/textconv disabled; transcript, command output, diff, and instruction context are bounded

Summary by CodeRabbit

  • New Features
    • Added “Continue in ChatGPT Pro…” thread action with packaged Markdown handoff, repository context, and clipboard support.
    • Introduced banked rate-limit reset credits with a confirm-then-consume flow and automatic state refresh.
    • Model-aware reasoning efforts now include Max and Ultra, and update per model selection.
  • Bug Fixes
    • Preserve server-defined ordering for reasoning options.
    • Improve consistency of reasoning-effort selection when models/providers change.
  • Documentation
    • Added end-to-end verification guides for ChatGPT Pro handoffs and model-aware reasoning controls.
    • Added manual test plans for banked rate-limit resets.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a ChatGPT Pro handoff flow that packages thread and repository context into Markdown, introduces per-model reasoning efforts including Max and Ultra, and adds banked rate-limit reset retrieval, consumption, and UI handling.

Changes

ChatGPT Pro handoff

Layer / File(s) Summary
Handoff Markdown generation
src/server/proHandoff.ts, src/server/proHandoff.test.ts
Builds bounded Markdown from thread turns, goals, repository state, instruction files, and tests excluding private reasoning content.
Handoff server route
src/server/codexAppServerBridge.ts
Adds bounded Git/context collection and a validated POST /codex-api/thread/pro-handoff route.
Thread menu handoff action
src/components/sidebar/SidebarThreadTree.vue, src/App.vue, src/api/codexGateway.ts, tests/git-worktrees-rollback/thread-menu-copy-chat-action.md
Adds the selected-thread menu action, browser handoff request, clipboard copy, ChatGPT navigation, notices, and manual verification steps.

Model-aware reasoning efforts

Layer / File(s) Summary
Model metadata and gateway APIs
src/types/codex.ts, src/api/codexGateway.ts, src/api/codexGateway.test.ts
Adds structured model options, supports Max and Ultra, normalizes reasoning metadata, and preserves server ordering.
Per-model selection state
src/composables/useDesktopState.ts, src/composables/useDesktopState.test.ts
Tracks model metadata, derives supported efforts, validates selections, and resets incompatible efforts when models change.
Composer model and effort controls
src/components/content/ThreadComposer.vue, src/App.vue, tests/providers-models/per-thread-model-selection.md
Passes structured model and effort options to composers and documents per-model picker behavior.

Banked rate-limit resets

Layer / File(s) Summary
Reset-credit contracts and gateway operations
src/types/codex.ts, src/api/codexGateway.ts, src/server/rateLimitDecodeRecovery.ts, src/api/codexGateway.test.ts, src/server/rateLimitDecodeRecovery.test.ts
Adds reset-credit normalization, account-state retrieval, idempotent consumption, and recovery payload support with tests.
Reset consumption state
src/composables/useDesktopState.ts, src/composables/useDesktopState.test.ts
Refreshes authoritative reset state, guards consumption, maps outcomes to notices, and exposes reset state and actions.
Banked reset controls
src/components/content/RateLimitStatus.vue, src/App.vue, src/style.css, tests/accounts-feedback-observability/*
Adds confirmation-based reset controls, notices, styling, app wiring, and manual test coverage.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SidebarThreadTree
  participant App
  participant CodexGateway
  participant CodexAppServerBridge
  participant ChatGPT
  User->>SidebarThreadTree: Select “Continue in ChatGPT Pro…”
  SidebarThreadTree->>App: Emit selected thread id
  App->>CodexGateway: Request handoff
  CodexGateway->>CodexAppServerBridge: POST thread/pro-handoff
  CodexAppServerBridge-->>CodexGateway: Markdown and ChatGPT URL
  CodexGateway-->>App: Handoff payload
  App->>App: Copy Markdown to clipboard
  App->>ChatGPT: Navigate opened window
Loading

Suggested reviewers: friuns

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: model-aware reasoning pickers and the new ChatGPT Pro handoff flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Model-aware reasoning picker + ChatGPT Pro handoff from selected thread

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Drive model + reasoning pickers from model/list metadata, preserving server ordering/defaults.
• Add “Continue in ChatGPT Pro…” to package thread/goal/repo context and open ChatGPT.
• Keep handoff bounded/read-only and exclude raw reasoning from exported transcript.
Diagram

graph TD
  A["codexGateway (model/list)"] --> B["useDesktopState"] --> C["ThreadComposer pickers"]
  D["Sidebar thread menu"] --> E["App.vue handler"] --> F["POST /codex-api/thread/pro-handoff"] --> G["proHandoff builder"]
  F --> H["Git + AGENTS snapshot"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Client-only handoff packaging (no server endpoint)
  • ➕ Avoids server-side git/file access concerns
  • ➕ Removes need for a new API route
  • ➖ Browser can’t reliably run git commands or access repo files without extra permissions
  • ➖ Harder to ensure consistent bounding/timeouts across platforms
2. Generate downloadable handoff artifact (file) instead of clipboard
  • ➕ Better for very large handoffs and auditability
  • ➕ Avoids clipboard permission/policy issues
  • ➖ Adds UI/UX complexity (download management, naming, storage location)
  • ➖ Still needs the same server-side snapshot logic
3. Integrate a first-class ChatGPT handoff protocol (if available)
  • ➕ Could avoid manual paste and reduce user error
  • ➕ Potentially richer structured context transfer
  • ➖ Dependent on external product/API capabilities that may not exist or may change
  • ➖ More vendor coupling than a Markdown handoff

Recommendation: The PR’s approach (server-built, bounded Markdown + clipboard copy + explicit user action) is the most practical given browser limitations and the need for deterministic truncation/timeouts. If clipboard policies become problematic, consider adding an optional ‘Download handoff’ fallback without changing the core server snapshot logic.

Files changed (13) +992 / -27

Enhancement (8) +736 / -26
App.vueWire Pro handoff action + notices; pass model metadata to composer +70/-3

Wire Pro handoff action + notices; pass model metadata to composer

• Adds a selected-thread event handler that requests a Pro handoff, copies Markdown to clipboard, and navigates a newly-opened tab to ChatGPT. Updates composer props to pass full model metadata and derived reasoning-effort options, and introduces a timed success/error toast for user feedback.

src/App.vue

codexGateway.tsNormalize model/list into UiModelOption; add Pro handoff API call +91/-9

Normalize model/list into UiModelOption; add Pro handoff API call

• Introduces 'getAvailableModels()' returning 'UiModelOption[]' (including supported efforts and defaults) and makes 'getAvailableModelIds()' return an ID array annotated with non-enumerable 'modelOptions'. Adds 'createChatGptProHandoff()' client API wrapper and expands reasoning-effort normalization to accept 'max'/'ultra'.

src/api/codexGateway.ts

ThreadComposer.vueConsume model metadata and dynamic reasoning-effort options +20/-10

Consume model metadata and dynamic reasoning-effort options

• Changes props to accept 'UiModelOption[]' and 'UiReasoningEffortOption[]', builds picker options from server metadata, and formats effort labels (including legacy casing).

src/components/content/ThreadComposer.vue

SidebarThreadTree.vueAdd “Continue in ChatGPT Pro…” thread-menu action +16/-0

Add “Continue in ChatGPT Pro…” thread-menu action

• Adds a new menu item that is enabled only for the currently selected thread, emits a 'continue-in-chatgpt-pro' event, and closes the menu on action.

src/components/sidebar/SidebarThreadTree.vue

useDesktopState.tsTrack availableModels and derive per-model reasoning-effort options +72/-3

Track availableModels and derive per-model reasoning-effort options

• Adds 'availableModels' state alongside 'availableModelIds', computes 'availableReasoningEfforts' from selected model metadata with a legacy fallback, and enforces a valid selected effort when the model changes. Integrates the new 'modelOptions' metadata returned by 'getAvailableModelIds()'.

src/composables/useDesktopState.ts

codexAppServerBridge.tsAdd /thread/pro-handoff endpoint with bounded git + AGENTS snapshotting +189/-0

Add /thread/pro-handoff endpoint with bounded git + AGENTS snapshotting

• Implements a POST endpoint that reads persisted turns and goal, validates an absolute thread working directory, snapshots git state using bounded/time-limited commands (with ext diff/textconv disabled), reads applicable 'AGENTS.md' files with size caps, and returns a Markdown handoff payload.

src/server/codexAppServerBridge.ts

proHandoff.tsBuild bounded Codex → ChatGPT Pro handoff Markdown +264/-0

Build bounded Codex → ChatGPT Pro handoff Markdown

• Introduces a formatter that produces a structured Markdown handoff including session metadata, goal, repo snapshot, applicable 'AGENTS.md', and a bounded persisted transcript. Explicitly omits reasoning items and bounds command output/transcript size with truncation markers.

src/server/proHandoff.ts

codex.tsAdd model metadata and reasoning-effort option types; extend efforts +14/-1

Add model metadata and reasoning-effort option types; extend efforts

• Extends 'ReasoningEffort' to include 'max'/'ultra' and adds 'UiModelOption' / 'UiReasoningEffortOption' types for transporting model/list metadata into the UI.

src/types/codex.ts

Tests (3) +192 / -1
codexGateway.test.tsAdd coverage for model/list reasoning-effort ordering (max/ultra) +47/-1

Add coverage for model/list reasoning-effort ordering (max/ultra)

• Extends gateway tests to validate that 'model/list' metadata preserves server-provided supported reasoning-effort order and includes 'max'/'ultra' when advertised.

src/api/codexGateway.test.ts

useDesktopState.test.tsTest model-aware reasoning picker behavior and effort reset +54/-0

Test model-aware reasoning picker behavior and effort reset

• Adds a regression test ensuring available reasoning efforts are derived from selected-model metadata (including 'max'/'ultra') and that switching models resets an unsupported selected effort to the model’s default.

src/composables/useDesktopState.test.ts

proHandoff.test.tsUnit test Pro handoff Markdown formatting and reasoning exclusion +91/-0

Unit test Pro handoff Markdown formatting and reasoning exclusion

• Adds tests verifying the handoff includes goal/transcript/repo/instructions, avoids '[object Object]' artifacts, and excludes raw reasoning items and summaries.

src/server/proHandoff.test.ts

Documentation (2) +64 / -0
thread-menu-copy-chat-action.mdDocument manual test plan for Pro handoff menu action +36/-0

Document manual test plan for Pro handoff menu action

• Adds step-by-step validation and expected results for the “Continue in ChatGPT Pro…” flow, including popup-blocked behavior and exclusions/truncation checks.

tests/git-worktrees-rollback/thread-menu-copy-chat-action.md

per-thread-model-selection.mdDocument manual test plan for model-aware reasoning-effort picker +28/-0

Document manual test plan for model-aware reasoning-effort picker

• Adds a manual checklist verifying server-ordered efforts (including 'Max'/'Ultra'), filtering per model, and correct reset to default on model switch.

tests/providers-models/per-thread-model-selection.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Dark overrides kept in App.vue 📘 Rule violation ⚙ Maintainability
Description
The new pro-handoff-notice dark-theme overrides are implemented as component-scoped :root.dark
CSS inside src/App.vue rather than placing the decisive overrides in src/style.css. This
violates the requirement to centralize dark-theme fixes for shared/large UI surfaces, increasing the
risk of fragmented or fragile theming behavior.
Code

src/App.vue[R6181-6187]

+:root.dark .pro-handoff-notice {
+  @apply border-emerald-800 bg-emerald-950 text-emerald-100;
+}
+
+:root.dark .pro-handoff-notice.is-error {
+  @apply border-rose-800 bg-rose-950 text-rose-100;
+}
Evidence
PR Compliance ID 3 requires decisive dark-theme overrides for shared/large UIs to live in
src/style.css. The PR adds new :root.dark .pro-handoff-notice and `:root.dark
.pro-handoff-notice.is-error rules directly inside src/App.vue`, which is precisely the
component-scoped pattern the rule prohibits as the sole implementation.

AGENTS.md: Decisive dark-theme overrides for shared/large UIs must be placed in src/style.css: AGENTS.md: Decisive dark-theme overrides for shared/large UIs must be placed in src/style.css
src/App.vue[6181-6187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The dark-theme overrides for the new `pro-handoff-notice` are implemented via component-scoped `:root.dark ...` rules inside `src/App.vue`, but compliance requires decisive dark-theme overrides for shared/large UIs to be placed in `src/style.css`.
## Issue Context
`pro-handoff-notice` is a global/shared UI notice (fixed overlay) and should follow centralized theming rules to avoid scattered dark-mode overrides.
## Fix Focus Areas
- src/App.vue[6181-6187]
- src/style.css[1-999999]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Handoff fetch lacks timeout 🐞 Bug ☼ Reliability
Description
createChatGptProHandoff() performs a POST to /codex-api/thread/pro-handoff without any
AbortSignal/timeout, so a stalled bridge/server can leave the “Packaging…” notice visible and the
pre-opened blank tab orphaned indefinitely. This is inconsistent with other gateway fetches that
already enforce timeouts.
Code

src/api/codexGateway.ts[R565-571]

+export async function createChatGptProHandoff(threadId: string, cwd: string): Promise<ChatGptProHandoff> {
+  const response = await fetch('/codex-api/thread/pro-handoff', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ threadId, cwd }),
+  })
+  const payload = await response.json() as { data?: Partial<ChatGptProHandoff>; error?: unknown }
Evidence
The new handoff fetch has no signal/timeout, while other calls in the same module explicitly bound
request time using AbortSignal.timeout, demonstrating the inconsistency and the unbounded hang risk.

src/api/codexGateway.ts[560-580]
src/api/codexGateway.ts[2033-2042]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`createChatGptProHandoff()` uses `fetch()` without a timeout/abort signal. If the request never resolves (hung local bridge, network stall), the UI flow in `App.vue` remains stuck after showing the packaging notice and may leave a blank popup tab open.
### Issue Context
Other gateway calls already bound fetch time (e.g. provider models). The handoff path is user-triggered and may involve server-side git inspection, so it should also be time-bounded.
### Fix Focus Areas
- src/api/codexGateway.ts[565-580]
### Suggested fix
- Add an `AbortSignal.timeout(...)` (or AbortController) to the fetch request.
- Convert abort errors into a friendly `Error` message so the existing `App.vue` catch path shows an actionable notice.
- Optionally include a slightly longer timeout than typical metadata calls (e.g. 15–30s) since handoff packaging can be heavier.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Pro handoff silent no-op 🐞 Bug ≡ Correctness
Description
The sidebar enables “Continue in ChatGPT Pro…” for any selected thread, but the App.vue handler
returns immediately when composerCwd is empty, resulting in a click that does nothing (no notice, no
error) for threads without a working directory. This can occur when the selected thread’s cwd is
missing/empty.
Code

src/App.vue[R4241-4243]

+async function onContinueInChatGptPro(threadId: string): Promise<void> {
+  if (threadId !== selectedThreadId.value || !composerCwd.value) return
+  const chatWindow = window.open('about:blank', '_blank')
Evidence
The menu item is only gated by “is selected”, but the handler requires a non-empty composerCwd
(derived from selectedThread.cwd) and returns before showing any notice, causing an enabled action
to do nothing for cwd-less threads.

src/components/sidebar/SidebarThreadTree.vue[627-644]
src/App.vue[1800-1803]
src/App.vue[4241-4246]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new menu action can be enabled but perform a silent early return when `composerCwd` is empty, producing a confusing no-op.
### Issue Context
- The menu item is disabled only when the thread is not selected.
- `composerCwd` is derived from `selectedThread.cwd` on the thread route; if that is empty, `onContinueInChatGptPro()` returns before showing any notice.
### Fix Focus Areas
- src/App.vue[4241-4246]
- src/App.vue[1800-1803]
- src/components/sidebar/SidebarThreadTree.vue[627-644]
### Suggested fix
Pick one:
1) In `onContinueInChatGptPro`, replace the silent return for missing `composerCwd` with `showProHandoffNotice('This thread has no working directory to package.', 'error')`.
2) Pass a `hasCwd`/`cwd` boolean down to `SidebarThreadTree` so it can disable the menu item (and set a title) when the selected thread lacks a cwd.
Either approach ensures the user gets immediate feedback instead of nothing happening.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Handoff accepts request cwd 🐞 Bug ⛨ Security
Description
The new /codex-api/thread/pro-handoff endpoint falls back to body.cwd when the persisted thread
cwd is missing, then uses that value to stat() the directory, read AGENTS.md, and run git commands
to build the snapshot included in the returned Markdown. This weakens the endpoint’s trust boundary
(client-controlled filesystem target for projectless threads) and also turns common invalid-path
cases into a 500 via the outer catch.
Code

src/server/codexAppServerBridge.ts[R8076-8083]

+          const requestedCwd = readNonEmptyString(body?.cwd)
+          const cwd = readNonEmptyString(thread?.cwd) || requestedCwd
+          if (!cwd || !isAbsolute(cwd)) {
+            setJson(res, 400, { error: 'The thread does not have a valid working directory' })
+            return
+          }
+          const cwdStat = await stat(cwd)
+          if (!cwdStat.isDirectory()) {
Evidence
The middleware explicitly chooses cwd from the request body when the thread has no cwd, then uses
it to stat the directory and to read instruction files and build repository/diff sections that are
returned in the Markdown response.

src/server/codexAppServerBridge.ts[8062-8093]
src/server/codexAppServerBridge.ts[4363-4391]
src/server/proHandoff.ts[191-215]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/codex-api/thread/pro-handoff` uses `cwd = thread.cwd || body.cwd`. When `thread.cwd` is missing, the caller can choose an arbitrary absolute directory for server-side inspection (stat, git, AGENTS.md reads), and failures (e.g. ENOENT) bubble to the outer catch as HTTP 500.
### Issue Context
This is a local-bridge endpoint, so practical exploitability depends on who can reach the bridge, but it still expands the set of filesystem locations the endpoint will inspect based solely on client input.
### Fix Focus Areas
- src/server/codexAppServerBridge.ts[8062-8093]
- src/server/codexAppServerBridge.ts[4363-4391]
- src/server/proHandoff.ts[191-215]
### Suggested fix
- Prefer removing the `body.cwd` fallback entirely: require `thread.cwd` to exist and be absolute; otherwise return 400 (with an explicit message).
- If a fallback is required for some workflow, validate `body.cwd` against an allowlist (e.g., workspace roots) and/or require it to match the stored thread cwd.
- Wrap `stat(cwd)` in a try/catch and return a controlled 4xx (e.g., 400) for invalid/unreadable directories.
This keeps the handoff snapshot aligned with the actual thread and avoids client-directed filesystem probing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/App.vue
Comment on lines +6181 to +6187
:root.dark .pro-handoff-notice {
@apply border-emerald-800 bg-emerald-950 text-emerald-100;
}

:root.dark .pro-handoff-notice.is-error {
@apply border-rose-800 bg-rose-950 text-rose-100;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Dark overrides kept in app.vue 📘 Rule violation ⚙ Maintainability

The new pro-handoff-notice dark-theme overrides are implemented as component-scoped :root.dark
CSS inside src/App.vue rather than placing the decisive overrides in src/style.css. This
violates the requirement to centralize dark-theme fixes for shared/large UI surfaces, increasing the
risk of fragmented or fragile theming behavior.
Agent Prompt
## Issue description
The dark-theme overrides for the new `pro-handoff-notice` are implemented via component-scoped `:root.dark ...` rules inside `src/App.vue`, but compliance requires decisive dark-theme overrides for shared/large UIs to be placed in `src/style.css`.

## Issue Context
`pro-handoff-notice` is a global/shared UI notice (fixed overlay) and should follow centralized theming rules to avoid scattered dark-mode overrides.

## Fix Focus Areas
- src/App.vue[6181-6187]
- src/style.css[1-999999]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/api/codexGateway.ts
Comment on lines +565 to +571
export async function createChatGptProHandoff(threadId: string, cwd: string): Promise<ChatGptProHandoff> {
const response = await fetch('/codex-api/thread/pro-handoff', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ threadId, cwd }),
})
const payload = await response.json() as { data?: Partial<ChatGptProHandoff>; error?: unknown }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Handoff fetch lacks timeout 🐞 Bug ☼ Reliability

createChatGptProHandoff() performs a POST to /codex-api/thread/pro-handoff without any
AbortSignal/timeout, so a stalled bridge/server can leave the “Packaging…” notice visible and the
pre-opened blank tab orphaned indefinitely. This is inconsistent with other gateway fetches that
already enforce timeouts.
Agent Prompt
### Issue description
`createChatGptProHandoff()` uses `fetch()` without a timeout/abort signal. If the request never resolves (hung local bridge, network stall), the UI flow in `App.vue` remains stuck after showing the packaging notice and may leave a blank popup tab open.

### Issue Context
Other gateway calls already bound fetch time (e.g. provider models). The handoff path is user-triggered and may involve server-side git inspection, so it should also be time-bounded.

### Fix Focus Areas
- src/api/codexGateway.ts[565-580]

### Suggested fix
- Add an `AbortSignal.timeout(...)` (or AbortController) to the fetch request.
- Convert abort errors into a friendly `Error` message so the existing `App.vue` catch path shows an actionable notice.
- Optionally include a slightly longer timeout than typical metadata calls (e.g. 15–30s) since handoff packaging can be heavier.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/App.vue
Comment on lines +4241 to +4243
async function onContinueInChatGptPro(threadId: string): Promise<void> {
if (threadId !== selectedThreadId.value || !composerCwd.value) return
const chatWindow = window.open('about:blank', '_blank')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Pro handoff silent no-op 🐞 Bug ≡ Correctness

The sidebar enables “Continue in ChatGPT Pro…” for any selected thread, but the App.vue handler
returns immediately when composerCwd is empty, resulting in a click that does nothing (no notice, no
error) for threads without a working directory. This can occur when the selected thread’s cwd is
missing/empty.
Agent Prompt
### Issue description
The new menu action can be enabled but perform a silent early return when `composerCwd` is empty, producing a confusing no-op.

### Issue Context
- The menu item is disabled only when the thread is not selected.
- `composerCwd` is derived from `selectedThread.cwd` on the thread route; if that is empty, `onContinueInChatGptPro()` returns before showing any notice.

### Fix Focus Areas
- src/App.vue[4241-4246]
- src/App.vue[1800-1803]
- src/components/sidebar/SidebarThreadTree.vue[627-644]

### Suggested fix
Pick one:
1) In `onContinueInChatGptPro`, replace the silent return for missing `composerCwd` with `showProHandoffNotice('This thread has no working directory to package.', 'error')`.
2) Pass a `hasCwd`/`cwd` boolean down to `SidebarThreadTree` so it can disable the menu item (and set a title) when the selected thread lacks a cwd.

Either approach ensures the user gets immediate feedback instead of nothing happening.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +8076 to +8083
const requestedCwd = readNonEmptyString(body?.cwd)
const cwd = readNonEmptyString(thread?.cwd) || requestedCwd
if (!cwd || !isAbsolute(cwd)) {
setJson(res, 400, { error: 'The thread does not have a valid working directory' })
return
}
const cwdStat = await stat(cwd)
if (!cwdStat.isDirectory()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Handoff accepts request cwd 🐞 Bug ⛨ Security

The new /codex-api/thread/pro-handoff endpoint falls back to body.cwd when the persisted thread
cwd is missing, then uses that value to stat() the directory, read AGENTS.md, and run git commands
to build the snapshot included in the returned Markdown. This weakens the endpoint’s trust boundary
(client-controlled filesystem target for projectless threads) and also turns common invalid-path
cases into a 500 via the outer catch.
Agent Prompt
### Issue description
`/codex-api/thread/pro-handoff` uses `cwd = thread.cwd || body.cwd`. When `thread.cwd` is missing, the caller can choose an arbitrary absolute directory for server-side inspection (stat, git, AGENTS.md reads), and failures (e.g. ENOENT) bubble to the outer catch as HTTP 500.

### Issue Context
This is a local-bridge endpoint, so practical exploitability depends on who can reach the bridge, but it still expands the set of filesystem locations the endpoint will inspect based solely on client input.

### Fix Focus Areas
- src/server/codexAppServerBridge.ts[8062-8093]
- src/server/codexAppServerBridge.ts[4363-4391]
- src/server/proHandoff.ts[191-215]

### Suggested fix
- Prefer removing the `body.cwd` fallback entirely: require `thread.cwd` to exist and be absolute; otherwise return 400 (with an explicit message).
- If a fallback is required for some workflow, validate `body.cwd` against an allowlist (e.g., workspace roots) and/or require it to match the stored thread cwd.
- Wrap `stat(cwd)` in a try/catch and return a controlled 4xx (e.g., 400) for invalid/unreadable directories.

This keeps the handoff snapshot aligned with the actual thread and avoids client-directed filesystem probing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/server/proHandoff.test.ts (1)

1-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the truncation/fallback branches.

Current tests only exercise the happy path and reasoning exclusion. The bounded-output guarantees (appendBounded at 360k chars, fencedText at 12k chars) and the repository: null / empty contextFiles fallback messages aren't covered, so a regression in the truncation math wouldn't be caught.

✅ Example additional test cases
it('falls back to the not-a-git-repo message when repository is null', () => {
  const markdown = buildChatGptProHandoff({ threadResult: { thread: {} }, repository: null })
  expect(markdown).toContain('The working directory is not inside a Git repository.')
})

it('truncates transcripts past the character limit', () => {
  const hugeText = 'x'.repeat(400_000)
  const markdown = buildChatGptProHandoff({
    threadResult: { thread: { turns: [{ items: [{ type: 'agentMessage', text: hugeText }] }] } },
  })
  expect(markdown).toContain('Transcript truncated at')
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/proHandoff.test.ts` around lines 1 - 91, Add tests in the
buildChatGptProHandoff suite covering the repository: null fallback message,
empty contextFiles fallback behavior, and transcript truncation with content
exceeding appendBounded’s 360k-character limit. Also cover fencedText’s
12k-character truncation path and assert the corresponding truncation markers,
while preserving existing happy-path and reasoning-exclusion coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/composables/useDesktopState.ts`:
- Line 1787: Update both fallback retry paths in useDesktopState around
applyFallbackModelSelection() to derive the fallback model’s compatible effort
only after the selection is applied, rather than reusing the previously captured
pending.effort. Pass that recalculated effort to both retry startThreadTurn
calls, and add a regression test covering an ultra turn falling back to a model
with a compatible default effort.

---

Nitpick comments:
In `@src/server/proHandoff.test.ts`:
- Around line 1-91: Add tests in the buildChatGptProHandoff suite covering the
repository: null fallback message, empty contextFiles fallback behavior, and
transcript truncation with content exceeding appendBounded’s 360k-character
limit. Also cover fencedText’s 12k-character truncation path and assert the
corresponding truncation markers, while preserving existing happy-path and
reasoning-exclusion coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 48f5770e-aee7-45c5-a8f2-611c0fe8853f

📥 Commits

Reviewing files that changed from the base of the PR and between fac2291 and ecc7f2b.

📒 Files selected for processing (13)
  • src/App.vue
  • src/api/codexGateway.test.ts
  • src/api/codexGateway.ts
  • src/components/content/ThreadComposer.vue
  • src/components/sidebar/SidebarThreadTree.vue
  • src/composables/useDesktopState.test.ts
  • src/composables/useDesktopState.ts
  • src/server/codexAppServerBridge.ts
  • src/server/proHandoff.test.ts
  • src/server/proHandoff.ts
  • src/types/codex.ts
  • tests/git-worktrees-rollback/thread-menu-copy-chat-action.md
  • tests/providers-models/per-thread-model-selection.md

ensureAvailableModelIds(normalizedModelId)
if (selectedThreadId.value === normalizedThreadId) {
selectedModelId.value = readModelIdForThread(selectedThreadId.value)
ensureSelectedReasoningEffortForModel()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Revalidate effort for fallback retries.

This resets the UI selection for the fallback model, but both fallback retry paths still submit the previously captured pending.effort. A turn started with ultra can therefore retry on the fallback model with unsupported ultra instead of its compatible default. Derive the fallback effort after applyFallbackModelSelection() and use it in both retry startThreadTurn calls; add an ultra fallback regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/composables/useDesktopState.ts` at line 1787, Update both fallback retry
paths in useDesktopState around applyFallbackModelSelection() to derive the
fallback model’s compatible effort only after the selection is applied, rather
than reusing the previously captured pending.effort. Pass that recalculated
effort to both retry startThreadTurn calls, and add a regression test covering
an ultra turn falling back to a model with a compatible default effort.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/App.vue (1)

4251-4272: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Prevent concurrent handoff generation.

Repeated menu actions before the request settles open multiple windows and start duplicate repository-inspection requests. Add an in-flight guard and clear it in finally; disable the triggering action while active.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.vue` around lines 4251 - 4272, Update onContinueInChatGptPro to use
an in-flight guard that returns immediately when a handoff is already being
generated, sets the guard before starting the async work, and clears it in a
finally block. Bind the triggering menu action’s disabled state to this guard so
repeated activations are prevented while the request is pending.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/codexGateway.test.ts`:
- Around line 124-127: Remove the duplicate requests declaration and its
corresponding duplicate requests.push call within the test case “uses the
selected opaque credit ID and caller-provided idempotency key,” leaving one
requests collection and one recording operation.

In `@src/api/codexGateway.ts`:
- Around line 1513-1525: The default idempotency key in
consumeRateLimitResetCredit must persist across ambiguous retries instead of
being regenerated on every call. Update the retry/attempt state around
consumeRateLimitResetCredit and createRateLimitResetIdempotencyKey so one key is
retained and reused until an authoritative refresh resolves the attempt, while
preserving explicit idempotencyKey behavior and existing normalization.

In `@src/components/content/RateLimitStatus.vue`:
- Around line 2-6: Update the RateLimitStatus template so resetNotice remains
rendered when resetCredits is null: move the notice rendering outside the
section gated by resetCredits, while preserving the existing snapshots and
refreshed-credits display conditions and the outer aside visibility check.

---

Outside diff comments:
In `@src/App.vue`:
- Around line 4251-4272: Update onContinueInChatGptPro to use an in-flight guard
that returns immediately when a handoff is already being generated, sets the
guard before starting the async work, and clears it in a finally block. Bind the
triggering menu action’s disabled state to this guard so repeated activations
are prevented while the request is pending.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b48aa2d-fbe7-4411-b407-350bb0df7167

📥 Commits

Reviewing files that changed from the base of the PR and between ecc7f2b and 5b07a44.

📒 Files selected for processing (12)
  • src/App.vue
  • src/api/codexGateway.test.ts
  • src/api/codexGateway.ts
  • src/components/content/RateLimitStatus.vue
  • src/composables/useDesktopState.test.ts
  • src/composables/useDesktopState.ts
  • src/server/rateLimitDecodeRecovery.test.ts
  • src/server/rateLimitDecodeRecovery.ts
  • src/style.css
  • src/types/codex.ts
  • tests/accounts-feedback-observability/banked-rate-limit-resets.md
  • tests/accounts-feedback-observability/index.md

Comment on lines +124 to +127
it('uses the selected opaque credit ID and caller-provided idempotency key', async () => {
const requests: Array<{ method: string; params: Record<string, unknown> }> = []
vi.stubGlobal('fetch', vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
requests.push(JSON.parse(String(init?.body)) as { method: string; params: Record<string, unknown> })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicate declarations.

requests is declared twice in the same scope, so TypeScript compilation fails. The duplicate requests.push(...) must also be removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/codexGateway.test.ts` around lines 124 - 127, Remove the duplicate
requests declaration and its corresponding duplicate requests.push call within
the test case “uses the selected opaque credit ID and caller-provided
idempotency key,” leaving one requests collection and one recording operation.

Comment thread src/api/codexGateway.ts
Comment on lines +1513 to +1525
export async function consumeRateLimitResetCredit(
creditId?: string,
idempotencyKey: string = createRateLimitResetIdempotencyKey(),
): Promise<UiRateLimitResetOutcome> {
const normalizedIdempotencyKey = idempotencyKey.trim()
if (!normalizedIdempotencyKey) throw new Error('A reset idempotency key is required')
const normalizedCreditId = creditId?.trim() ?? ''
const params: { idempotencyKey: string; creditId?: string } = {
idempotencyKey: normalizedIdempotencyKey,
}
if (normalizedCreditId) params.creditId = normalizedCreditId

const result = await callRpc<unknown>('account/rateLimitResetCredit/consume', params)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the idempotency key across ambiguous retries.

Each call creates a fresh key, so a retry after a server-side success but lost response is a new consumption. This can redeem another credit when no creditId is supplied. Retain and reuse one attempt key until an authoritative refresh resolves the attempt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/codexGateway.ts` around lines 1513 - 1525, The default idempotency
key in consumeRateLimitResetCredit must persist across ambiguous retries instead
of being regenerated on every call. Update the retry/attempt state around
consumeRateLimitResetCredit and createRateLimitResetIdempotencyKey so one key is
retained and reused until an authoritative refresh resolves the attempt, while
preserving explicit idempotencyKey behavior and existing normalization.

Comment on lines +2 to +6
<aside
v-if="snapshots.length > 0 || resetCredits !== null || resetNotice !== null"
class="rate-limit-status"
aria-live="polite"
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep reset outcomes visible when refreshed credits are omitted.

A refresh can set resetCredits to null after the state layer has set resetNotice, but the notice is inside v-if="resetCredits !== null". The outer aside then renders empty, so success or error feedback disappears. Render the notice outside that section.

Proposed fix
-      <p
-        v-if="resetNotice"
-        class="banked-reset-notice"
-        :data-type="resetNotice.type"
-        :role="resetNotice.type === 'error' ? 'alert' : 'status'"
-      >
-        {{ resetNotice.text }}
-      </p>
     </section>
+
+    <p
+      v-if="resetNotice"
+      class="banked-reset-notice"
+      :data-type="resetNotice.type"
+      :role="resetNotice.type === 'error' ? 'alert' : 'status'"
+    >
+      {{ resetNotice.text }}
+    </p>

Also applies to: 33-98

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/content/RateLimitStatus.vue` around lines 2 - 6, Update the
RateLimitStatus template so resetNotice remains rendered when resetCredits is
null: move the notice rendering outside the section gated by resetCredits, while
preserving the existing snapshots and refreshed-credits display conditions and
the outer aside visibility check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant